{T}

管理设计篇之"分布式锁" [2026重制版]

核心变更说明:本文基于原本文档第52篇重写,全面更新至2026年技术栈。新增 Redis 7.x/Redisson 3.x、ZooKeeper 3.8、etcd v3.5 对比分析,深入剖析 Redlock 算法争议与替代方案,补充性能基准测试数据(来源:Redis.io 官方 Benchmark),提供完整的生产级代码示例。


一、问题背景:为什么需要分布式锁

在单机多线程环境下,我们使用 synchronizedReentrantLock 或数据库行锁来保证共享资源的互斥访问。然而,当系统演变为分布式架构后,服务部署在多台机器上,甚至跨机房、跨可用区部署,传统的进程内锁机制完全失效。

1.1 典型业务场景

场景描述并发风险
库存扣减电商秒杀、商品库存扣减超卖问题
金额操作银行转账、账户余额更新数据不一致
任务调度定时任务防止重复执行任务重复执行
幂等控制API 接口防重复提交重复下单
配置变更配置中心热更新配置冲突

1.2 分布式锁的核心要求

根据 Redis 官方文档 对分布式锁的定义,一个合格的分布式锁必须满足以下三个核心特性:

  • 互斥性(Mutual Exclusion):在任意时刻,只有一个客户端能持有锁
  • 避免死锁(Deadlock Free):即使持有锁的客户端崩溃或网络分区,锁最终仍能被其他客户端获取
  • 容错性(Fault Tolerance):只要大部分锁服务节点正常运行,客户端就能进行加锁和解锁操作

此外,生产环境还需要考虑:

  • 可重入性:同一线程可多次获取同一把锁
  • 阻塞/非阻塞:支持获取锁失败时的策略选择
  • 公平性:按请求顺序获取锁(可选)

二、技术原理与架构演进

2.1 分布式锁的实现方式全景图

图表渲染中…

2.2 主流方案对比

特性Redis (Redisson)ZooKeeper (Curator)etcd (JetCD)数据库
性能⭐⭐⭐⭐⭐ 极高⭐⭐⭐ 中等⭐⭐⭐⭐ 高⭐⭐ 低
可靠性⭐⭐⭐⭐ 高(需集群)⭐⭐⭐⭐⭐ 极高(ZAB协议)⭐⭐⭐⭐⭐ 极高(Raft)⭐⭐⭐ 中等
一致性最终一致强一致强一致强一致
实现复杂度低(SDK成熟)中等
可重入支持✅ 原生支持✅ 支持✅ 支持❌ 需自行实现
Watch/通知Pub/Sub原生 WatchWatch轮询
适用场景高并发、性能敏感强一致、协调服务K8s原生、云原生已有DB依赖

三、Redis 分布式锁深度剖析

3.1 基础实现:SET NX PX

根据 Redis 官方文档,正确的加锁命令如下:

bash
# 加锁:仅在 key 不存在时设置,并设置过期时间
SET resource_name my_random_value NX PX 30000

参数说明:

  • NX:只在 key 不存在时才设置(Not eXist)
  • PX:设置过期时间,单位毫秒(此处为30秒)
  • my_random_value:全局唯一的标识符,用于安全释放锁

解锁脚本(Lua)

lua
-- 比对 value,确保只有锁的持有者才能释放
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

3.2 为什么需要随机值?

这是为了解决误释放问题。考虑以下场景:

图表渲染中…

解决方案:每个客户端生成唯一标识(如 UUID + 线程ID),释放锁时先比对 value。

3.3 Redlock 算法:争议与现状

Redis 作者 Antirez 在 Redlock 文章 中提出了 Redlock 算法,试图在 Redis 单节点故障时保证安全性。

Redlock 算法流程

  1. 获取当前时间戳 T1
  2. 相同顺序依次向 N 个 Redis 实例请求加锁
  3. 计算耗时,只有在大多数节点(N/2 + 1)获取成功且总耗时小于锁的有效时间时,才算获取成功
  4. 如果获取失败,向所有实例发送解锁指令

Redlock 的争议

Martin Kleppmann(剑桥大学分布式系统研究员)在 文章 中指出 Redlock 的根本缺陷

问题核心:Redis 使用异步复制,在主节点宕机时可能发生锁状态丢失。即使使用 Redlock,也无法在 CAP 定理中同时满足一致性和可用性。

Martin 的论证(GC 暂停场景):

图表渲染中…

Antirez 的回应:承认 Redlock 不适合需要强一致性的场景(如金融交易),但在多数业务场景下是足够的。

3.4 生产级推荐:Redisson

Redisson 是 Java 生态中最成熟的 Redis 客户端,内置了完善的分布式锁实现。

Maven 依赖

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.27.0</version>
</dependency>

配置示例

java
@Configuration
public class RedissonConfig {
 
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 单节点模式
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setPassword("your_password")
              .setDatabase(0)
              .setConnectionPoolSize(64)
              .setConnectionMinimumIdleSize(24)
              .setIdleConnectionTimeout(10000)
              .setConnectTimeout(5000)
              .setTimeout(3000)
              .setRetryAttempts(3)
              .setRetryInterval(1500);
        return Redisson.create(config);
    }
 
    // 集群模式(推荐生产使用)
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClusterClient() {
        Config config = new Config();
        config.useClusterServers()
              // 集群节点列表
              .addNodeAddress("redis://node1:6379",
                             "redis://node2:6379",
                             "redis://node3:6379")
              .setPassword("your_password")
              .setScanInterval(5000); // 集群状态扫描间隔
        return Redisson.create(config);
    }
}

可重入锁使用示例

java
@Service
public class InventoryService {
 
    @Autowired
    private RedissonClient redissonClient;
 
    /**
     * 扣减库存 - 使用可重入锁
     * @param 商品ID
     * @param 扣减数量
     * @return 是否成功
     */
    public boolean deductStock(String productId, int quantity) {
        // 定义锁的名称(建议包含业务标识)
        String lockKey = "lock:inventory:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
 
        try {
            // 尝试加锁,最多等待 10 秒,锁定后 30 秒自动释放
            // watchdog 机制会自动续期
            boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
 
            if (!acquired) {
                log.warn("获取库存锁失败,商品ID: {}", productId);
                return false;
            }
 
            // ====== 业务逻辑开始 ======
            // 1. 查询库存
            Integer currentStock = getStockFromDB(productId);
 
            if (currentStock < quantity) {
                log.info("库存不足,商品ID: {},当前库存: {}", productId, currentStock);
                return false;
            }
 
            // 2. 扣减库存
            boolean success = updateStockInDB(productId, currentStock - quantity);
 
            if (success) {
                log.info("扣减库存成功,商品ID: {},数量: {}", productId, quantity);
            }
            // ====== 业务逻辑结束 ======
 
            return success;
 
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取锁被中断", e);
            return false;
        } finally {
            // 仅当锁是当前线程持有的才释放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Redisson 锁的 Watchdog 机制

Redisson 的看门狗机制是其核心亮点:

图表渲染中…

关键点

  • 只有在不指定 leaseTime 时才会启用 watchdog
  • watchdog 默认每 10 秒 续期一次,续期为 30 秒
  • 业务执行完毕或显式调用 unlock() 后停止续期
  • 适用于执行时间不确定的业务场景

四、ZooKeeper 分布式锁

4.1 原理:临时顺序节点

ZooKeeper 通过 ZAB(Zookeeper Atomic Broadcast)协议保证强一致性,其分布式锁实现基于临时顺序节点

图表渲染中…

4.2 Curator 实现示例

Apache Curator 是 ZooKeeper 的客户端框架,提供了高级别的锁API。

Maven 依赖

xml
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.5.0</version>
</dependency>

代码示例

java
@Service
public class ZkDistributedLockService {
 
    private final InterProcessMutex lock;
 
    public ZkDistributedLockService(CuratorFramework client) {
        // 创建互斥锁,路径为 /locks/my-lock
        this.lock = new InterProcessMutex(client, "/locks/my-lock");
    }
 
    public void doWithLock() {
        try {
            // 尝试获取锁,最多等待 10 秒
            boolean acquired = lock.acquire(10, TimeUnit.SECONDS);
 
            if (acquired) {
                try {
                    // 执行业务逻辑
                    executeBusinessLogic();
                } finally {
                    lock.release();
                }
            } else {
                log.warn("获取 ZooKeeper 锁超时");
            }
        } catch (Exception e) {
            log.error("ZooKeeper 锁异常", e);
        }
    }
}

4.3 ZooKeeper vs Redis 锁对比

维度ZooKeeperRedis
一致性模型CP(强一致)AP(最终一致)
性能(QPS)~10,000(受限于磁盘IO)~100,000+(纯内存)
锁释放会话断开自动释放依赖 TTL / Watchdog
实现复杂度较高(需维护会话)较低(SDK成熟)
适用场景强一致需求(金融、支付)高并发、性能敏感

五、etcd 分布式锁

5.1 简介

etcd 是 Cloud Native Computing Foundation (CNCF) 的毕业项目,采用 Raft 共识算法,是 Kubernetes 的数据存储后端。etcd v3.5+ 提供了成熟的并发原语。

5.2 核心原理:Lease + Transaction

java
// 使用 JetCD 客户端(推荐)
// Maven: io.etcd:jetcd-core:0.7.6
 
@Service
public class EtcdDistributedLockService {
 
    private final Client etcdClient;
    private final Lease leaseClient;
    private final Lock lockClient;
 
    public EtcdDistributedLockService() {
        this.etcdClient = Client.builder()
                .endpoints("http://etcd1:2379", "http://etcd2:2379")
                .build();
        this.leaseClient = etcdClient.getLeaseClient();
        this.lockClient = etcdClient.getLockClient();
    }
 
    public void doWithLock(String lockName) {
        try {
            // 获取一个 30 秒的租约
            LeaseGrantResponse leaseResp = leaseClient.grant(30).get();
            long leaseId = leaseResp.getID();
 
            // 使用租约创建锁
            LockResponse lockResp = lockClient.lock(
                    ByteSequence.from(lockName, StandardCharsets.UTF_8),
                    leaseId
            ).get();
 
            try {
                // 业务逻辑
                businessLogic();
            } finally {
                // 解锁
                lockClient.unlock(lockResp.getKey()).get();
            }
        } catch (Exception e) {
            log.error("etcd 锁操作异常", e);
        }
    }
}

5.3 etcd 的优势

  • Raft 协议:强一致性保证,无单点故障
  • Watch 机制:高效的变更通知
  • 与 Kubernetes 原生集成:云原生应用首选
  • gRPC 接口:高性能、跨语言支持

六、性能基准测试

以下数据来源于各项目官方 Benchmark 和社区测试报告:

6.1 吞吐量对比(单机 QPS)

方案加锁 QPS解锁 QPS平均延迟P99 延迟测试环境
Redis (单节点)~120,000~120,0000.5ms2msRedis 7.2, 8核32G
Redis (Cluster)~400,000~400,0000.8ms5ms3主3从, 同上
Redisson RLock~95,000~95,0001.0ms8ms含 watchdog 开销
ZooKeeper~12,000~12,0005ms25msZK 3.8, 8核16G
etcd v3.5~18,000~18,0003ms15ms3节点集群
MySQL (FOR UPDATE)~3,000~3,00015ms80msMySQL 8.0, SSD

数据来源:Redis BenchmarkZooKeeper Wikietcd Benchmark

6.2 不同并发下的延迟表现

图表渲染中…

图例:蓝色=Redis,橙色=etcd,绿色=ZooKeeper


七、实战案例:电商库存扣减

7.1 架构设计

图表渲染中…

7.2 完整代码实现

java
/**
 * 库存扣减服务 - 生产级实现
 *
 * 设计要点:
 * 1. 使用 Redisson 可重入锁
 * 2. 双层校验(缓存 + 数据库 CAS)
 * 3. 异步扣减,提升吞吐
 * 4. 完善的日志与监控
 */
@Service
@Slf4j
public class StockDeductionServiceImpl implements StockDeductionService {
 
    @Autowired
    private RedissonClient redissonClient;
 
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
 
    @Autowired
    private ProductMapper productMapper;
 
    @Autowired
    private ApplicationEventPublisher eventPublisher;
 
    /** 锁等待时间(秒) */
    private static final long LOCK_WAIT_TIME = 5;
    /** 锁持有时间(秒) */
    private static final long LOCK_LEASE_TIME = 10;
    /** 库存缓存 Key 前缀 */
    private static final String STOCK_CACHE_PREFIX = "stock:";
 
    @Override
    public StockDeductResult deduct(String productId, int quantity, String orderId) {
        long startTime = System.currentTimeMillis();
 
        // 1. 快速路径:检查缓存中的库存是否充足
        String cacheKey = STOCK_CACHE_PREFIX + productId;
        String stockStr = stringRedisTemplate.opsForValue().get(cacheKey);
 
        if (stockStr != null) {
            int cachedStock = Integer.parseInt(stockStr);
            if (cachedStock < quantity) {
                return StockDeductResult.fail("库存不足", cachedStock);
            }
        }
 
        // 2. 获取分布式锁
        String lockKey = "lock:stock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
 
        try {
            boolean locked = lock.tryLock(
                    LOCK_WAIT_TIME,
                    LOCK_LEASE_TIME,
                    TimeUnit.SECONDS
            );
 
            if (!locked) {
                log.warn("获取库存锁失败,productId: {}, orderId: {}", productId, orderId);
                return StockDeductResult.fail("系统繁忙,请稍后重试", -1);
            }
 
            // 3. 二次校验库存(双重检查)
            stockStr = stringRedisTemplate.opsForValue().get(cacheKey);
            int currentStock = (stockStr != null) ? Integer.parseInt(stockStr)
                                                  : getStockFromDB(productId);
 
            if (currentStock < quantity) {
                // 刷新缓存
                refreshStockCache(productId, currentStock);
                return StockDeductResult.fail("库存不足", currentStock);
            }
 
            // 4. 数据库 CAS 更新(乐观锁)
            int affected = productMapper.decreaseStockWithCAS(productId, quantity, currentStock);
 
            if (affected > 0) {
                // 5. 更新缓存
                int newStock = currentStock - quantity;
                stringRedisTemplate.opsForValue().set(cacheKey, String.valueOf(newStock), 1, TimeUnit.HOURS);
 
                // 6. 发布事件(异步处理后续逻辑)
                eventPublisher.publishEvent(new StockDeductedEvent(productId, quantity, orderId));
 
                long cost = System.currentTimeMillis() - startTime;
                log.info("库存扣减成功,productId: {}, orderId: {}, 耗时: {}ms", productId, orderId, cost);
 
                return StockDeductResult.success(newStock);
            } else {
                // CAS 失败,说明有并发修改
                log.warn("库存扣减CAS失败,productId: {}", productId);
                return StockDeductResult.fail("库存变化,请重试", -1);
            }
 
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return StockDeductResult.fail("操作中断", -1);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
 
    /**
     * 从数据库查询库存(带兜底)
     */
    private int getStockFromDB(String productId) {
        Product product = productMapper.selectById(productId);
        if (product == null) {
            throw new BusinessException("商品不存在");
        }
        return product.getStock();
    }
 
    /**
     * 刷新库存缓存
     */
    private void refreshStockCache(String productId, int stock) {
        stringRedisTemplate.opsForValue().set(
                STOCK_CACHE_PREFIX + productId,
                String.valueOf(stock),
                1, TimeUnit.HOURS
        );
    }
}

7.3 Mapper 层(CAS 实现)

java
@Mapper
public interface ProductMapper {
 
    /**
     * 使用乐观锁扣减库存
     * UPDATE ... SET stock = stock - #{quantity} WHERE id = #{id} AND stock = #{expectedStock}
     */
    @Update("UPDATE product SET stock = stock - #{quantity}, " +
            "update_time = NOW() " +
            "WHERE id = #{productId} AND stock >= #{quantity} AND stock = #{expectedStock}")
    int decreaseStockWithCAS(@Param("productId") String productId,
                              @Param("quantity") int quantity,
                              @Param("expectedStock") int expectedStock);
}

八、2026 最佳实践总结

8.1 选型决策树

图表渲染中…

8.2 生产环境 Checklist

  • 锁粒度:锁的 Key 要足够细(如 lock:order:{userId}:{orderId}),避免锁范围过大
  • 超时设置:根据业务执行时间合理设置 leaseTime,建议设置为正常耗时的 3~5 倍
  • 锁的 value:必须使用全局唯一标识(UUID + ThreadId),防止误释放
  • finally 解锁:必须在 finally 块中释放锁,并判断是否为当前线程持有
  • 可重入:同一线程内嵌套调用时使用可重入锁
  • 监控告警:监控锁等待时间、获取失败率,设置合理的阈值告警
  • 降级策略:获取锁失败时的降级处理(返回友好提示、进入排队等)
  • 集群部署:Redis 使用 Cluster 或 Sentinel 模式,避免单点故障
  • 避免嵌套锁:多个资源的加锁要按固定顺序,防止死锁
  • 日志记录:记录锁的获取/释放/等待耗时,便于排查问题

8.3 常见陷阱与规避

陷阱说明规避方法
锁未释放忘记在 finally 中 unlock使用 try-with-resources 或标准模板
锁时间太短业务未完成锁就过期了使用 watchdog 或合理估算时间
锁时间太长故障时长时间不释放设置合理的超时时间
Redis 主从切换丢锁异步复制导致锁丢失使用 Redlock 或接受风险
GC 导致的时钟跳跃STW 导致锁提前过期使用 ZooKeeper/etcd 或增大缓冲
网络分区脑裂情况下的安全问题采用多数派决策(Paxos/Raft)

九、延伸资源

官方文档

经典论文与讨论

开源项目


本文版本:2026 重制版 | 基于本文档第52篇原文重构 最后更新:2026-06-06 | 技术栈:Redis 7.2 / Redisson 3.27 / ZooKeeper 3.8 / etcd 3.5